Skip to content

Add keyboard-accessible tooltip for truncated ActionList.Description#7529

Merged
TylerJDev merged 42 commits intomainfrom
liuliu/add-keyboard-accessible-tooltip-for-actionlist
Mar 6, 2026
Merged

Add keyboard-accessible tooltip for truncated ActionList.Description#7529
TylerJDev merged 42 commits intomainfrom
liuliu/add-keyboard-accessible-tooltip-for-actionlist

Conversation

@liuliu-dev
Copy link
Contributor

Related issue https://github.com/github/accessibility-audits/issues/14802

Changelog

When ActionList.Description is rendered with truncate, the full text was only visible via a native title attribute — which only appears on mouse hover, not keyboard focus (WCAG SC 2.1.1).

This PR adds a Primer <Tooltip> at the ActionList.Item level that shows the full description text on both hover and keyboard focus when the description is actually truncated.

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

Merge checklist

@changeset-bot
Copy link

changeset-bot bot commented Feb 12, 2026

🦋 Changeset detected

Latest commit: b3dbbad

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Feb 12, 2026
@github-actions
Copy link
Contributor

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Or, apply the integration-tests: skipped manually label to skip these checks.

@github-actions github-actions bot requested a deployment to storybook-preview-7529 March 3, 2026 18:13 Abandoned
@github-actions github-actions bot requested a deployment to storybook-preview-7529 March 3, 2026 18:20 Abandoned
}

& + .SubGroup {
& ~ .SubGroup {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious about this change/why its needed 👀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change fixes a NavList SubGroup collapse failure(https://github.com/primer/react/actions/runs/22242503448/job/64350808521?pr=7529).
The collapse styling used + to find .SubGroup right after .ActionListContent. When Tooltip wraps the trigger, it adds an extra element between them, so .SubGroup is no longer the immediate next sibling and the + selector does not match.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense! My assumption is that there will only be one .SubGroup within the container, so there's no risk of it applying to other sub-groups that are also children of the same container.

@TylerJDev
Copy link
Member

Looks good! I noticed one small bug on the truncation story:

Screen.Recording.2026-03-04.at.6.12.06.AM.mov

It seems that the third option doesn't load a tooltip, and the cursor changes rapidly 🤔

@liuliu-dev
Copy link
Contributor Author

Looks good! I noticed one small bug on the truncation story:

Screen.Recording.2026-03-04.at.6.12.06.AM.mov
It seems that the third option doesn't load a tooltip, and the cursor changes rapidly 🤔

@TylerJDev Good catch, thanks!

This was exposed after we removed _privateDisableTooltip={!text} in 3488872#diff-21bd620cf266fd1b3eb8d73399ee032c406b9bf49eac6f598ad65c9389ddb515. Without it, ConditionalTooltip would mount/unmount <Tooltip> based on whether text was set, and that mount/unmount cycle was causing an infinite re-render loop for complex (non-string) descriptions.

The root cause was two separate useEffects in Description.tsx. the first extracted text content from the DOM and set computedTitle, which changed effectiveTitle, which triggered the second effect, which re-triggered the cycle. For simple string children this never happened because effectiveTitle was stable from the first render. I fixed it by merging the two effects into one effect that reads the DOM text and sets the truncatedText.

One question: do you think we should also add _privateDisableTooltip={!text}back so the Tooltip wrapper stays mounted at all times(when it's enabled)? That way the DOM tree shape is stable between setting the text in Description.

@TylerJDev
Copy link
Member

One question: do you think we should also add _privateDisableTooltip={!text}back so the Tooltip wrapper stays mounted at all times(when it's enabled)? That way the DOM tree shape is stable between setting the text in Description.

Thank you for the explanation! I guess with _privateDisableTooltip, are we only adding back the changes in the commit you referred to? Or are there other changes that would be included with this? 🤔

@liuliu-dev
Copy link
Contributor Author

One question: do you think we should also add _privateDisableTooltip={!text}back so the Tooltip wrapper stays mounted at all times(when it's enabled)? That way the DOM tree shape is stable between setting the text in Description.

Thank you for the explanation! I guess with _privateDisableTooltip, are we only adding back the changes in the commit you referred to? Or are there other changes that would be included with this? 🤔

This would be the change:

-  if (!enabled || !text) {
+  if (!enabled) {
      return children
    }
    return (
-     <Tooltip ref={forwardedRef} text={text || ''} direction="e" delay="medium">
+     <Tooltip ref={forwardedRef} text={text || ''} direction="e" delay="medium" _privateDisableTooltip={!text}>
        {children}
      </Tooltip>
    )

Since text is set asynchronously in a useEffect or non-string descriptions, using if (!enabled || !text) to conditionally mount/unmount the Tooltip causes a DOM tree change between renders. With _privateDisableTooltip={!text}, the Tooltip stays mounted but disabled when text is empty, so the tree is always stable.

@TylerJDev
Copy link
Member

Since text is set asynchronously in a useEffect or non-string descriptions, using if (!enabled || !text) to conditionally mount/unmount the Tooltip causes a DOM tree change between renders. With _privateDisableTooltip={!text}, the Tooltip stays mounted but disabled when text is empty, so the tree is always stable.

Gotcha! We can go about adding it back then!

Copy link
Member

@TylerJDev TylerJDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! 🚀

@primer-integration
Copy link

👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/15252

@primer-integration
Copy link

Integration test results from github/github-ui:

Passed  CI   Passed
Passed  VRT   Passed
Waiting  Projects   Waiting

@primer primer bot mentioned this pull request Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants